/*
   Copyright 2009 Abdulla Abdurakhmanov (abdulla@latestbit.com)
   Original sources are available at www.latestbit.com

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package org.latestbit.smartobjects.io;

import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;

/**
 * Extension (over aggregation) of standard java.nio.ByteBuffer with resizable behavior
 * @author abdulla-a
 *
 */
public class ResizableByteBuffer {

	ByteBuffer buffer;
	int initialCapacity, maxCapacity, minCapacityIncrementStep;
	
	public ResizableByteBuffer(boolean isDirect, int initialCapacity, int maxCapacity, int minCapacityIncrementStep) {
		this.maxCapacity = maxCapacity;
		this.initialCapacity = initialCapacity;
		if(isDirect)
			this.buffer = ByteBuffer.allocateDirect(initialCapacity);
		else
			this.buffer = ByteBuffer.allocate(initialCapacity);
		this.minCapacityIncrementStep = minCapacityIncrementStep;
	}
	
	ResizableByteBuffer(ResizableByteBuffer src) {
		this.maxCapacity = src.maxCapacity;
		this.initialCapacity = src.initialCapacity;
		this.minCapacityIncrementStep = src.minCapacityIncrementStep;
		this.buffer = src.buffer.duplicate();
	}
	
	public ResizableByteBuffer(ByteBuffer src) {
		this.maxCapacity = src.capacity();
		this.initialCapacity = src.capacity();
		this.minCapacityIncrementStep = src.capacity()/10;
		this.buffer = src;
	}
	
	public int getInitialCapacity() {
		return this.initialCapacity;
	}
	
	public int getMinCapacityIncrementStep() {
		return this.minCapacityIncrementStep;
	}

	public int getMaxCapacity() {
		return this.maxCapacity;
	}
	
	public void setMaxCapacity(int maxCapacity) {
		this.maxCapacity = maxCapacity;
	}	
	
	public ByteBuffer getBuffer() {
		// dumpTrace();
		return this.buffer;
	}
		
	/* private void dumpTrace() {		
		if(traceEnabled)
			Thread.dumpStack();	
	}*/

	public ResizableByteBuffer duplicate() {
		return new ResizableByteBuffer(this);
	}

	private void checkBufferSize(int elementSize) {
		checkBufferSize(buffer.position(), elementSize);
	}

	private void checkBufferSize ( int index, int elementSize ) {
		//dumpTrace();
		
		int remaining = buffer.limit() - index;
		if( remaining < elementSize ) {
			
			int capacityIncrementStep = elementSize - remaining > minCapacityIncrementStep ? elementSize - remaining : minCapacityIncrementStep;
			if(capacityIncrementStep == 77) {
				throw new Error();
			}
			int newCapacity = buffer.limit() + capacityIncrementStep;
			
			if(newCapacity > maxCapacity)
				throw new BufferUnderflowException();
			ByteBuffer newBuffer = null;
			if(this.buffer.isDirect()) {
				newBuffer = ByteBuffer.allocateDirect(newCapacity);				
			}
			else
				newBuffer = ByteBuffer.allocate(newCapacity);
			this.buffer.flip();
			newBuffer.put(this.buffer);
			this.buffer = newBuffer;
		}
	}	
	
	public ResizableByteBuffer put(byte b) {
		checkBufferSize(Byte.SIZE / 8);
		buffer.put(b);
		return this;
	}

	public ResizableByteBuffer put(int index, byte b) {
		checkBufferSize(index, Byte.SIZE / 8);
		buffer.put(index, b);
		return this;
	}
	
	public ResizableByteBuffer put(byte[] src) {
		checkBufferSize((Byte.SIZE / 8) * src.length );
		buffer.put(src);
		return this;
	}
	
	public ResizableByteBuffer put(byte[] src, int offset, int length)  {
		checkBufferSize((Byte.SIZE / 8) * length );
		buffer.put(src, offset, length);
		return this;
	}
	
	public ResizableByteBuffer put(ByteBuffer src) {
		checkBufferSize(src.limit() - src.position());
		buffer.put(src);
		return this;
	}
	
	public ResizableByteBuffer putChar(char value) {
		checkBufferSize(2);
		buffer.putChar(value);
		return this;
	}

	
	public ResizableByteBuffer putChar(int index, char value) {
		checkBufferSize(index, Character.SIZE / 8);
		buffer.putChar(index, value);
		return this;
	}

	
	public ResizableByteBuffer putDouble(double value) {
		checkBufferSize(Double.SIZE / 8);
		buffer.putDouble(value);
		return this;
	}

	
	public ResizableByteBuffer putDouble(int index, double value) {
		checkBufferSize(index, Double.SIZE / 8);
		buffer.putDouble(index, value);
		return this;
	}

	
	public ResizableByteBuffer putFloat(float value) {
		checkBufferSize(Float.SIZE / 8);
		buffer.putFloat(value);
		return this;

	}

	
	public ResizableByteBuffer putFloat(int index, float value) {
		checkBufferSize(index, Float.SIZE / 8);
		buffer.putFloat(index, value);
		return this;
	}

	
	public ResizableByteBuffer putInt(int value) {
		checkBufferSize(Integer.SIZE / 8);
		buffer.putInt(value);
		return this;
	}

	
	public ResizableByteBuffer putInt(int index, int value) {
		checkBufferSize(index, Integer.SIZE / 8);
		buffer.putInt(index, value);
		return this;
	}

	
	public ResizableByteBuffer putLong(long value) {
		checkBufferSize(Long.SIZE / 8);
		buffer.putLong(value);
		return this;
	}

	
	public ResizableByteBuffer putLong(int index, long value) {
		checkBufferSize(index, Long.SIZE / 8);
		buffer.putLong(index, value);
		return this;
	}

	
	public ResizableByteBuffer putShort(short value) {
		checkBufferSize(Short.SIZE / 8);
		buffer.putShort(value);
		return this;
	}

	
	public ResizableByteBuffer putShort(int index, short value) {
		checkBufferSize(index, Short.SIZE / 8);
		buffer.putShort(index, value);
		return this;
	}
	
	public void prepareToSize(int size) {
		checkBufferSize( size );
	}	

}
